package sg.edu.nus.iss.billing.controller;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import sg.edu.nus.iss.billing.constant.ErrorCodeConstants;
import sg.edu.nus.iss.billing.entity.Account;
import sg.edu.nus.iss.billing.entity.Bill;
import sg.edu.nus.iss.billing.entity.ChargeComponent;
import sg.edu.nus.iss.billing.entity.PaymentHistory;
import sg.edu.nus.iss.billing.entity.Subscription;
import sg.edu.nus.iss.billing.entity.service.CableTVSubscription;
import sg.edu.nus.iss.billing.entity.service.DigitalVoiceSubscription;
import sg.edu.nus.iss.billing.entity.service.MobileVoiceSubscription;
import sg.edu.nus.iss.billing.entity.service.RateUnit;
import sg.edu.nus.iss.billing.entity.service.RateVAService;
import sg.edu.nus.iss.billing.entity.service.Service;
import sg.edu.nus.iss.billing.entity.service.VASSubscription;
import sg.edu.nus.iss.billing.entity.transaction.CallTransaction;
import sg.edu.nus.iss.billing.entity.transaction.CallType;
import sg.edu.nus.iss.billing.entity.transaction.Transaction;
import sg.edu.nus.iss.billing.exception.ControllerException;
import sg.edu.nus.iss.billing.localization.MessageKeyManager;
import sg.edu.nus.iss.billing.util.DateUtil;

/**
 * @author Daniel
 *
 */
public class BillCalculator {
	
	private Account account;
	private String billMonth;
	private String billYear;
	private Date startBillRangeDate;
	private Date endBillRangeDate;
	
	private Map<Subscription, List<Subscription>> subscriptionMap;
	
	public BillCalculator(Account account, String month, String year) {
		this.account = account;
		this.billMonth = month;
		this.billYear = year;
		this.subscriptionMap = new LinkedHashMap<Subscription, List<Subscription>>();
	}
	
	public Date getStartBillRangeDate(int day, int month, int year) {
		return getBillRangeDate(day, month, year, "LOWER");
	}
	
	public Date getEndBillRangeDate(int day, int month, int year) {
		return getBillRangeDate(day, month, year, "UPPER");
	}
	
	public Date getBillRangeDate(int day, int month, int year, String bounds) {
		if ("LOWER".equals(bounds)) {
			Calendar cal = Calendar.getInstance();
			cal.set(Calendar.MONTH, month);
			cal.add(Calendar.MONTH, -1);
			cal.set(Calendar.DAY_OF_MONTH, day);
			if (month == 0) {
				cal.set(Calendar.YEAR, year);
				cal.add(Calendar.YEAR, -1);
			}
			cal.set(Calendar.HOUR_OF_DAY, cal.getActualMinimum(Calendar.HOUR_OF_DAY));
			cal.set(Calendar.MILLISECOND, cal.getActualMinimum(Calendar.MILLISECOND));
			cal.set(Calendar.MINUTE, cal.getActualMinimum(Calendar.MINUTE));
			cal.set(Calendar.SECOND, cal.getActualMinimum(Calendar.SECOND));
			return cal.getTime();
		} else if ("UPPER".equals(bounds)) {
			Calendar cal = Calendar.getInstance();
			cal.set(Calendar.MONTH, month);
			cal.set(Calendar.DAY_OF_MONTH, day);
			cal.set(Calendar.YEAR, year);
			cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
			cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));
			cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
			cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
			return cal.getTime();			
		} else {
			return null;
		}
	}
	
	public void getChargableSubscription(List<Subscription> subscriptionList, Date startBillRangeDate, Date endBillRangeDate) throws Exception {
		for (int i=0; i < subscriptionList.size(); i++) {
			Subscription sub = subscriptionList.get(i);
			Date subStartDate = sub.getSubscriptionDate();
			Date subEndDate = sub.getTerminationDate();
			if (subEndDate == null) {
				subEndDate = DateUtil.getNonTerminatedEndDate();
				sub.setTerminationDate(subEndDate);
			}
			if (isOverlap(startBillRangeDate, endBillRangeDate, subStartDate, subEndDate)) {
				if (sub instanceof DigitalVoiceSubscription ||
					sub instanceof MobileVoiceSubscription ||
					sub instanceof CableTVSubscription) {
					subscriptionMap.put(sub, new ArrayList<Subscription>());
				} else {
					//VAS Subscription cases
					VASSubscription vasSub = (VASSubscription)sub;
					ArrayList<Subscription> vasSubList = (ArrayList<Subscription>)subscriptionMap.get(vasSub.getPlanSubscription());
					if (vasSubList != null) {
						vasSubList.add(vasSub);
					} else {
						throw new Exception("Cannot link the VAS correctly with plan subscription");
					}
				}
			}
		}
	}
	
	public boolean isOverlap(Date startDate1, Date endDate1, Date startDate2, Date endDate2) {
		return startDate1.before(endDate2) && startDate2.before(endDate1);
	}
	
	
	public List<PaymentHistory> getPreviousPaymentHistory(List<PaymentHistory> paymentHistory, Date startDate, Date endDate) {
		List<PaymentHistory> paymentHistoryList = new ArrayList<PaymentHistory>();
		if (paymentHistory != null) {
			for (int i=0; i < paymentHistory.size(); i++) {
				PaymentHistory history = paymentHistory.get(i);
				if (DateUtil.isDateWithin(history.getPaymentDate(), startDate, endDate)) {
					paymentHistoryList.add(history);
				}
			}
		}
		return paymentHistoryList;
	}
	
	public BigDecimal getPaymentHistoryTotal(List<PaymentHistory> paymentHistory) {
		BigDecimal result = new BigDecimal(0);
		for (int i=0; i < paymentHistory.size(); i++) {
			result = result.add(paymentHistory.get(i).getPaymentAmt());
		}
		return result;
	}
	
	public Map<Service, List<Service>> getServicesMapping() {
		Map<Service, List<Service>> serviceMap = new HashMap<Service, List<Service>>();
		Iterator<Subscription> iter = this.subscriptionMap.keySet().iterator();
		while (iter.hasNext()) {
			Subscription planSubscription = iter.next();
			List<Subscription> subSubscription = this.subscriptionMap.get(planSubscription);
			List<Service> subServices = new ArrayList<Service>();
			for (int i=0; i < subSubscription.size(); i++) {
				subServices.add(subSubscription.get(i).getService());
			}
			serviceMap.put(planSubscription.getService(), subServices);
		}
		return serviceMap;
	}
	
	public BigDecimal proratedCalculation(BigDecimal monthlyCharges, Date startDate, Date endDate) {
		long dayDiff = DateUtil.diffInDays(startDate, endDate);
		if (monthlyCharges != null) {
			BigDecimal proratedCharges = monthlyCharges.multiply(new BigDecimal(dayDiff)).divide(new BigDecimal(31),2,BigDecimal.ROUND_DOWN);
			return proratedCharges; 
		} else {
			return new BigDecimal(0);
		}
	}
	
	public Map<String, List<ChargeComponent>> populatePlanChargeComponent(String type, Map<String, List<ChargeComponent>> result,Subscription sub, Bill bill) {
		List<ChargeComponent> partialResult = new ArrayList<ChargeComponent>();
		BigDecimal totalSubscriptionCharge = new BigDecimal(0);
		String chargeKey = "";
		ChargeComponent cComp = new ChargeComponent();
		boolean isLocal = false;
		BigDecimal localRate = new BigDecimal(0);
		RateUnit localRateUnit = null;
		
		if (type.equals("DV")) {
			DigitalVoiceSubscription dvSub = (DigitalVoiceSubscription)sub;
			chargeKey = dvSub.getService().getServiceName()+ "_" + "DVSERVICE" + "_" + dvSub.getServicePhoneNumber();
			cComp.setChargeKey(chargeKey);
			cComp.setChargeGroupKey(Bill.GROUP_SUBS_LINE);
			cComp.setChargeDescription("Line");
			if (DateUtil.isDateWithin(dvSub.getSubscriptionDate(), this.startBillRangeDate, this.endBillRangeDate)) {
				BigDecimal proratedFee = proratedCalculation(dvSub.getService().getFixedMonthlyCharge(), dvSub.getSubscriptionDate(), this.endBillRangeDate);
				cComp.setChargeAmt(proratedFee);
			} else if (DateUtil.isDateWithin(dvSub.getTerminationDate(), this.startBillRangeDate, this.endBillRangeDate) ) {
				BigDecimal proratedFee = proratedCalculation(dvSub.getService().getFixedMonthlyCharge(), this.startBillRangeDate, dvSub.getTerminationDate());
				cComp.setChargeAmt(proratedFee);
			} else {
				cComp.setChargeAmt(dvSub.getService().getFixedMonthlyCharge());
			}
			//localRate = dvSub.getService().getServiceRate();
			//localRateUnit = dvSub.getService().getRateUnit();
			isLocal = true;
		} else if (type.equals("MV")) {
			MobileVoiceSubscription mvSub = (MobileVoiceSubscription)sub;
			chargeKey = mvSub.getService().getServiceName()+ "_" + "MVSERVICE" + "_" + mvSub.getServicePhoneNumber();
			cComp.setChargeKey(chargeKey);
			cComp.setChargeDescription("Line");
			cComp.setChargeGroupKey(Bill.GROUP_SUBS_LINE);			
			if (DateUtil.isDateWithin(mvSub.getSubscriptionDate(), this.startBillRangeDate, this.endBillRangeDate)) {
				BigDecimal proratedFee = proratedCalculation(mvSub.getService().getFixedMonthlyCharge(), mvSub.getSubscriptionDate(), this.endBillRangeDate);
				cComp.setChargeAmt(proratedFee);
			} else if (DateUtil.isDateWithin(mvSub.getTerminationDate(), this.startBillRangeDate, this.endBillRangeDate) ) {
				BigDecimal proratedFee = proratedCalculation(mvSub.getService().getFixedMonthlyCharge(), this.startBillRangeDate, mvSub.getTerminationDate());
				cComp.setChargeAmt(proratedFee);
			} else {
				cComp.setChargeAmt(mvSub.getService().getFixedMonthlyCharge());
			}
			//localRate = mvSub.getService().getServiceRate();
			//localRateUnit = mvSub.getService().getRateUnit();
			isLocal = true;
		} else if (type.equals("CTV")) {
			CableTVSubscription ctvSub = (CableTVSubscription)sub;
			chargeKey = ctvSub.getService().getServiceName()+ "_" + "CTVSERVICE" + "_" + ctvSub.getId();
			cComp.setChargeKey(chargeKey);
			cComp.setChargeDescription("Subscription charges");
			cComp.setChargeGroupKey(Bill.GROUP_SUBS_LINE);
			if (DateUtil.isDateWithin(ctvSub.getSubscriptionDate(), this.startBillRangeDate, this.endBillRangeDate)) {
				BigDecimal proratedFee = proratedCalculation(ctvSub.getService().getFixedMonthlyCharge(), ctvSub.getSubscriptionDate(), this.endBillRangeDate);
				cComp.setChargeAmt(proratedFee);
			} else if (DateUtil.isDateWithin(ctvSub.getTerminationDate(), this.startBillRangeDate, this.endBillRangeDate) ) {
				BigDecimal proratedFee = proratedCalculation(ctvSub.getService().getFixedMonthlyCharge(), this.startBillRangeDate, ctvSub.getTerminationDate());
				cComp.setChargeAmt(proratedFee); 
			} else {
				cComp.setChargeAmt(ctvSub.getService().getFixedMonthlyCharge());
			}
		}
		partialResult.add(cComp);
		totalSubscriptionCharge = totalSubscriptionCharge.add(cComp.getChargeAmt());
		
		List<Subscription> vasSubList = this.subscriptionMap.get(sub);
		boolean isIDD = false;
		BigDecimal iddRate = new BigDecimal(0);
		RateUnit iddRateUnit = null;
		boolean isRoaming = false;
		BigDecimal roamingRate = new BigDecimal(0);
		RateUnit roamingRateUnit = null;
		
		BigDecimal channelsTotal = new BigDecimal(0);

		for (int i=0; i < vasSubList.size(); i++) {
			VASSubscription vasSub = (VASSubscription)vasSubList.get(i);
			cComp = new ChargeComponent();
			cComp.setChargeKey(chargeKey);
			if (vasSub.isBasicSvc()) {
				cComp.setChargeDescription(vasSub.getService().getServiceName() + " (Basic)");
			} else {
				cComp.setChargeDescription(vasSub.getService().getServiceName());
			}
			cComp.setChargeGroupKey(Bill.GROUP_VAS_LINE);
			if (vasSub.isBasicSvc()) {
				cComp.setChargeAmt(new BigDecimal(0));
			} else {
				cComp.setChargeAmt(vasSub.getService().getFixedMonthlyCharge());
			}
			boolean ignoreAddToVASList = false;
			if (vasSub.getService() instanceof RateVAService) {
				RateVAService rateSvc = (RateVAService)vasSub.getService();
				if (CallType.IDD.equals(rateSvc.getCallType())) {
					iddRate = rateSvc.getServiceRate();
					iddRateUnit = rateSvc.getRateUnit();
					isIDD = true;
				}
				if (CallType.ROAMING.equals(rateSvc.getCallType())) {
					roamingRate = rateSvc.getServiceRate();
					roamingRateUnit = rateSvc.getRateUnit();
					isRoaming = true;
				}
				if (CallType.LOCAL.equals(rateSvc.getCallType())) {
					localRate = rateSvc.getServiceRate();
					localRateUnit = rateSvc.getRateUnit();
					isLocal = true;
					ignoreAddToVASList = true;
				}
			} else {
				//do nothing
			}
			if (!ignoreAddToVASList) {
				partialResult.add(cComp);
			}
			totalSubscriptionCharge = totalSubscriptionCharge.add(cComp.getChargeAmt());
		}
		
		cComp = new ChargeComponent();
		cComp.setChargeAmt(totalSubscriptionCharge);
		cComp.setChargeKey(chargeKey);
		cComp.setChargeGroupKey(Bill.GROUP_SUBS);
		cComp.setChargeDescription(Bill.SUBS_TOTAL);
		partialResult.add(cComp);
		
		bill.setThisBillCharges(bill.getThisBillCharges().add(totalSubscriptionCharge));
		
		List<CallTransaction> chargableTransaction = calculateCharges(account.getTransactions(), this.startBillRangeDate, this.endBillRangeDate);
		BigDecimal iddCallTotalCharge = new BigDecimal(0);
		BigDecimal localCallTotalCharge = new BigDecimal(0);
		BigDecimal roamingCallTotalCharge = new BigDecimal(0);
		for (int i=0; i < chargableTransaction.size(); i++) {
			CallTransaction callTxn = chargableTransaction.get(i);
			if (callTxn.getCallType().equals(CallType.IDD)) {
				if (isIDD) {
					iddCallTotalCharge =  iddRate.multiply(new BigDecimal(callTxn.getDurationInSeconds()));
				}
			}
			if (callTxn.getCallType().equals(CallType.ROAMING)) {
				if (isRoaming) {
					roamingCallTotalCharge = roamingRate.multiply(new BigDecimal(callTxn.getDurationInSeconds()));
				}
			}
			if (callTxn.getCallType().equals(CallType.LOCAL)) {
				if (isLocal) {
					localCallTotalCharge = localRate.multiply(new BigDecimal(callTxn.getDurationInSeconds()));
				}
			}
		}
		
		if (isIDD) {
			cComp = new ChargeComponent();
			cComp.setChargeKey(chargeKey);
			cComp.setChargeGroupKey(Bill.GROUP_SUBS_USAGE);
			cComp.setChargeDescription(Bill.IDD_USAGE_TOTAL);
			cComp.setChargeAmt(iddCallTotalCharge);
			partialResult.add(cComp);
		}
		
		if (isRoaming) {
			cComp = new ChargeComponent();
			cComp.setChargeKey(chargeKey);
			cComp.setChargeGroupKey(Bill.GROUP_SUBS_USAGE);
			cComp.setChargeDescription(Bill.ROAMING_USAGE_TOTAL);
			cComp.setChargeAmt(roamingCallTotalCharge);
			partialResult.add(cComp);
		}

		if (isLocal) {
			cComp = new ChargeComponent();
			cComp.setChargeKey(chargeKey);
			cComp.setChargeGroupKey(Bill.GROUP_SUBS_USAGE);
			cComp.setChargeDescription(Bill.LOCAL_USAGE_TOTAL);
			cComp.setChargeAmt(localCallTotalCharge);
			partialResult.add(cComp);
		}
		
		if (isIDD || isRoaming || isLocal) {
			cComp = new ChargeComponent();
			cComp.setChargeKey(chargeKey);
			cComp.setChargeGroupKey(Bill.GROUP_SUBS);
			cComp.setChargeDescription(Bill.USAGE_TOTAL);
			BigDecimal totalUsageCharges = iddCallTotalCharge.add(roamingCallTotalCharge).add(localCallTotalCharge);
			cComp.setChargeAmt(totalUsageCharges);
			partialResult.add(cComp);
			bill.setThisBillCharges(bill.getThisBillCharges().add(totalUsageCharges));
		}
		
		result.put(chargeKey, partialResult);
		return result;

	}

	public List<CallTransaction> calculateCharges(List<Transaction> transactions, Date startDate, Date endDate) {
		List<CallTransaction> transactionList = new ArrayList<CallTransaction>();
		if (transactions != null) {
			for (int i=0; i < transactions.size(); i++) {
				Transaction transaction = transactions.get(i);
				if (transaction instanceof CallTransaction) {
					CallTransaction cTxn = (CallTransaction) transaction;
					if (DateUtil.isDateWithin(cTxn.getCallTime(), startDate, endDate)) {
						transactionList.add(cTxn);
					}
				}
			}
		}
		return transactionList;
	}
	
	public Map<String, List<ChargeComponent>> populateChargeComponentMap(Bill bill) {
		Map<String, List<ChargeComponent>> result = new LinkedHashMap<String, List<ChargeComponent>>();
		if (this.subscriptionMap != null && this.subscriptionMap.size() > 0) {
			Iterator<Subscription> iter = this.subscriptionMap.keySet().iterator();
			while (iter.hasNext()) {
				Subscription sub = iter.next();
				if (sub instanceof DigitalVoiceSubscription) {
					result = populatePlanChargeComponent("DV", result, sub, bill);
				} else if (sub instanceof MobileVoiceSubscription) {
					result = populatePlanChargeComponent("MV", result, sub, bill);
				} else if (sub instanceof CableTVSubscription) {
					result = populatePlanChargeComponent("CTV", result, sub, bill);
				}
			}
		}
		return result;
	}
	
	public Bill calculate() throws ControllerException {
		Bill bill = new Bill();
		
		try {
			SubscriptionController subsController = new SubscriptionController();
			List<Subscription> subsList = subsController.getSubscriptionByAccNo(account.getAccountNumber());
			account.setSubscriptionList(subsList);
			
			bill.setAccountNumber(account.getAccountNumber());
			bill.setClonedCustomer(account.getCustomer());
	
			int cutoffDay = Integer.parseInt(MessageKeyManager.getMessage("system.billingcutoffday"));
			int month = Integer.parseInt(this.billMonth);
			int year = Integer.parseInt(this.billYear);
			Date billingDate = DateUtil.getDate(cutoffDay, month, year);
			bill.setBillingDate(billingDate);
			
			startBillRangeDate = getStartBillRangeDate(cutoffDay, month, year);
			endBillRangeDate = getEndBillRangeDate(cutoffDay, month, year);
			bill.setBillingDueDate(DateUtil.addDateByDay(billingDate, 20));
			bill.setCutoffStartDate(startBillRangeDate);
			bill.setCutoffEndDate(endBillRangeDate);
			
			if (account.getBalance() != null) {
				bill.setPreviousBalance(account.getBalance());
			}
			
			List<PaymentHistory> paymentHistoryList = getPreviousPaymentHistory(account.getPaymentHistories(), startBillRangeDate, endBillRangeDate);
			bill.setPaymentHistoryList(paymentHistoryList);
			bill.setPreviousPayment(getPaymentHistoryTotal(paymentHistoryList));
	 
			getChargableSubscription(account.getSubscriptionList(), startBillRangeDate, endBillRangeDate);
			bill.setChargeComponentMap(populateChargeComponentMap(bill));
			BigDecimal calculation = bill.getPreviousBalance().subtract(bill.getPreviousPayment()).add(bill.getThisBillCharges());
			BigDecimal gstCharges = bill.getThisBillCharges().multiply(new BigDecimal(7)).divide(new BigDecimal(100));
			bill.setCurrentGSTCharge(gstCharges);
			bill.setTotalBalance(calculation);
		} catch (ControllerException e) {
			throw e;
		} catch (Exception e) {
			throw new ControllerException(ErrorCodeConstants.UNKNOWN_ERROR, e.getMessage());
		}
		return bill;
	}

}
